package pulpfizz.physics.contact;

import java.util.ArrayList;
import java.util.HashMap;

import org.jbox2d.dynamics.Steppable;
import org.jbox2d.dynamics.World;

import pulpfizz.physics.Actor;
import pulpfizz.physics.NewWorld;
import pulpfizz.pulp.FizzScene;
import pulpfizz.pulp.body.PhysicsLayer;

/**
 * <p>
 * A dispatcher for pairwise listening of collision events.  Turned off by default
 * (for performance) unless a listener is registered, at which point it is automatically
 * turned on.  Reporting is always turned on when a listener is registered, so if you
 * want to turn it off, make sure you do so after all listeners have registered.
 * </p>
 * <p>
 * Types of registrations include:
 * <ul>
 * <li>Collisions involving a particular Actor
 * <li>Collisions between two specific Actors
 * <li>Collisions involving a particular group number
 * <li>Collisions between two specific groups
 * <li>Collisions between a given Actor and group
 * </ul>
 * </p>
 * <p>
 * Group ids are integers for convenience; internally they are implemented with flags,
 * though you should almost never have to deal with the flags themselves, merely use the
 * methods on Actor to add/remove from the appropriate groups.  Registration must use
 * the group number, not the group flags.
 * </p>
 * <p>
 * It is highly recommended that you create a class for handling group indexes by "name", e.g.
 * </p>
 * <pre>
 * public class Groups {
 *   static public final int DANGEROUS = 0;
 *   static public final int PICKUP = 1;
 *   static public final int PLAYER = 2;
 *   static public final int DAMAGEABLE = 3;
 *   static public final int PLAYS_SOUND = 4; 
 * }
 * </pre>
 * <p>
 * A basic usage example of ContactEventDispatcher follows, which
 * assumes you're using the pulpfizz.physics.NewWorld as your world -
 * if this is not the case, see that class for the implementation details.
 * </p>
 * <pre>
 * public class PulpFizzContactDispatchScene extends CameraControlledScene {
 *     ...
 *     private PhysicsLayer physics;
 *     ...
 *     
 *     public void load() {
 *         ... //initialization of physics, etc. omitted
 *         NewWorld world = physics.getWorld();
 *         
 *         Body bodyA, bodyB;
 *         ... //initialization of bodyA, bodyB omitted
 *         
 *         Actor aActor = new Actor();
 *         Actor bActor = new Actor();
 *         aActor.setName("Body A");
 *         bActor.setName("Body B");
 *         aActor.addToGroup(1);
 *         bActor.addToGroup(2);
 *         a.setUserData(aActor);
 *         b.setUserData(bActor);
 *
 *         //Create a contact listener that just prints out ADD events (new contacts)
 *         ContactEventListener listener = new ContactEventListener() {
 *            public void trigger(ContactData c) {
 *              if (c.state != ContactData.ADD) return;
 *              Actor a1 = (Actor)c.shape1.getBody().getUserData();
 *              Actor a2 = (Actor)c.shape2.getBody().getUserData();
 *              System.out.println(a1.getName() + " collided with " + a2.getName() + " at "+c.position);
 *            }
 *         };
 *
 *         // As of this point, contact event dispatching is turned off,
 *         // since no listeners are registered with the dispatcher.
 *		  
 *         // Register a listener for contacts between aActor and bActor
 *         world.getContactEventDispatcher().registerActorActorListener(fieldActor.getID(), bActor.getID(), listener);
 *
 *         // Handling has been automatically turned on.  If we'd like, we can turn it off:
 *         world.getContactEventDispatcher().setDispatchEvents(false);
 *         // But see below...
 *
 *         // Register a listener for all events involving group 1
 *         world.getContactEventDispatcher().registerGroupListener(1, listener);
 *
 *         // Even though we turned off dispatching a few lines back, it's on again here,
 *         // since we registered another listener.  If you *really* don't want it on,
 *         // you can turn it off after all your listeners have registered, which
 *         // in this example would be right here.  We're going to leave it on, though.
 *         //world.getContactEventDispatcher().setDispatchEvents(false);
 *     }
 * }
 * </pre>
 */
public class ContactEventDispatcher implements Steppable {
	//Note: pair listeners are always registered symmetrically,
	//so dispatching only needs to happen in the natural order.
	
	private ContactStore contactStore;
	
	//Note that we use a different structure for groups
	//because there are only 32 groups, so using a hash map
	//as a sparse array really doesn't save anything.
	private HashMap<Integer,ArrayList<ContactEventListener>> actorGroupListeners;
	private HashMap<Integer,ArrayList<ContactEventListener>> actorActorListeners;
	private HashMap<Integer,ArrayList<ContactEventListener>> actorListeners;
	private HashMap<Integer,ArrayList<ContactEventListener>> groupGroupListeners;
	private ArrayList<ArrayList<ContactEventListener>> groupListeners;
	
	private boolean m_dispatchEvents;
	
	public ContactEventDispatcher(NewWorld nw) {
		this(nw,nw); //NewWorld extends World and implements ContactStore
	}
	
	public ContactEventDispatcher(World w, ContactStore cs) {
		m_dispatchEvents = false;
		contactStore = cs;
		actorGroupListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		actorActorListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		actorListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		groupGroupListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		groupListeners = new ArrayList<ArrayList<ContactEventListener>>();
		for (int i=0; i<Actor.MAX_GROUPS; ++i) {
			groupListeners.add(new ArrayList<ContactEventListener>());
		}
		w.registerPostStep(this);
	}
	
	/**
	 * Clears out every registered listener.  Also turns off event dispatching,
	 * since it will be re-enabled the moment another listener is created.
	 */
	public void removeAllListeners() {
		m_dispatchEvents = true;
		actorGroupListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		actorActorListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		actorListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		groupGroupListeners = new HashMap<Integer,ArrayList<ContactEventListener>>();
		groupListeners = new ArrayList<ArrayList<ContactEventListener>>();
		for (int i=0; i<Actor.MAX_GROUPS; ++i) {
			groupListeners.add(new ArrayList<ContactEventListener>());
		}
	}
	
	/** 
	 * Set to false if you do not wish for contact events to be dispatched
	 * when step is called. Even if this is set to false, you can always
	 * manually send events using dispatchEvent(ContactData) and emitEvents().
	 */
	public void setDispatchEvents(boolean tf) {
		m_dispatchEvents = tf;
	}
	
	/** Is the dispatcher currently sending events? */
	public boolean isDispatchingEvents() {
		return m_dispatchEvents;
	}
	
	/**
	 * Send this contact event out to all interested listeners.
	 * Usually you should not need to call this yourself.
	 * @param c The ContactData to send
	 */
	public void dispatchEvent(ContactData c) {
		
		Actor a1, a2;
		try {
			a1 = (Actor)(c.shape1.getBody().getUserData());
			a2 = (Actor)(c.shape2.getBody().getUserData());
		} catch (NullPointerException npe) {
			throw new NullPointerException("Null pointer exception - no Actor was " +
					"attached as userdata to a body.  NewWorld::createBody() automatically " +
					"generates valid Actor objects, so make sure you've either used that or " +
					"created and attached your own Actor object.");
		}
		int a1id = a1.getID();
		int a2id = a2.getID();
		sendEventMessages(actorListeners,a1id,c);
		sendEventMessages(actorListeners,a2id,c);
		sendEventMessages(actorActorListeners,a1id,a2id,c);
		int flags3 = a1.getGroupFlags();
		int flags4 = a2.getGroupFlags();
		int groupsToSendTo = flags3 | flags4;
		sendActorGroupEventMessages(a1id,flags4,c);
		sendActorGroupEventMessages(a2id,flags3,c);
		sendGroupEventMessages(groupsToSendTo,c);
		sendGroupGroupEventMessages(flags3,flags4,c);
	}
	

	/**
	 * Add a listener that will report all contact events involving a certain
	 * group of shapes or actors.
	 * @param group The group number to listen for (NOT the group flags)
	 * @param listener
	 */
	public void registerGroupListener(int group, ContactEventListener listener) {
		assert(group < Actor.MAX_GROUPS);
		m_dispatchEvents = true;
		groupListeners.get(group).add(listener);
	}
	
	/**
	 * Remove a group listener.
	 * @param group Group number the listener was listening for
	 * @param listener The listener to remove
	 */
	public void unregisterGroupListener(int group, ContactEventListener listener) {
		assert(group < Actor.MAX_GROUPS);
		m_dispatchEvents = true;
		groupListeners.get(group).remove(listener);
	}

	private void sendActorGroupEventMessages(int actorID, int groupFlags, ContactData c) {
		if (groupFlags == 0) return;
		int[] groups = getGroups(groupFlags);
		int group = 0;
		int i = 0;
		while( (group = groups[i++]) != -1) {
			int hash = hash(actorID,group);
			ArrayList<ContactEventListener> toTrigger = actorGroupListeners.get(hash);
			if (toTrigger != null) {
				int toTriggerLength = toTrigger.size();
				for (int k=0; k<toTriggerLength; ++k) {
					toTrigger.get(k).trigger(c);
				}
			}
		}
	}
	
	private void sendGroupEventMessages(int groupFlags,ContactData c) {
		if (groupFlags == 0) return;
		int mask = 1 << 0;
		for (int i=0; i<Actor.MAX_GROUPS; ++i) {
			if ( (mask & groupFlags) == mask ) {
				// Fire event
				ArrayList<ContactEventListener> toTrigger = groupListeners.get(i);
				int toTriggerLength = toTrigger.size();
				for (int j=0; j<toTriggerLength; ++j) {
					toTrigger.get(j).trigger(c);
				}
			}
			mask = mask << 1;
		}
	}
	

	
	//TODO: optimize, optimize, optimize - this is pretty damn slow as is, since
	//it goes through everything even if there are no handlers registered.
	private void sendGroupGroupEventMessages(int groupFlags1, int groupFlags2, ContactData c) {
		if (groupFlags1 == 0 || groupFlags2 == 0) return;
		int[] groups1 = getGroups(groupFlags1);
		int[] groups2 = getGroups(groupFlags2);
		int i = 0;
		int g1 = 0;
		int g2 = 0;
//		for (int g=0; g<groups1.length; ++g) {
//			System.out.println(groups1[g]);
//		}
		while ( (g1 = groups1[i++]) != -1) {
			int j = 0;
			while ( (g2 = groups2[j++]) != -1) {
				int hash = hash(g1,g2);
				ArrayList<ContactEventListener> toTrigger = groupGroupListeners.get(hash);
				if (toTrigger != null) {
					int toTriggerLength = toTrigger.size();
					for (int k=0; k<toTriggerLength; ++k) {
						toTrigger.get(k).trigger(c);
						//System.out.println(i+" " +j+" " +k);
					}
				} else {
					//System.out.println("skipped group "+g1+" + "+g2);
				}
			}
		}
	}
	
	private int[] getGroups(int groupFlag) {
		int[] res = new int[Actor.MAX_GROUPS+1];
		int count = 0;
		for (int i=0; i<Actor.MAX_GROUPS; ++i) {
			int toCheck = 1<<i;
			if ((groupFlag & toCheck) == toCheck) res[count++] = i;
		}
		while (count < Actor.MAX_GROUPS+1) {
			res[count++] = -1;
		}
		return res;
	}
	
	private void sendEventMessages(HashMap<Integer,ArrayList<ContactEventListener>> hashMap, int id1, int id2, ContactData c) {
		int hashed = hash(id1,id2);
		ArrayList<ContactEventListener> list = hashMap.get(hashed);
		if (list == null) return;
		//System.out.println(list.size());
		for (int i=0; i<list.size(); ++i) {
			list.get(i).trigger(c);
		}
	}
	
	private void sendEventMessages(HashMap<Integer,ArrayList<ContactEventListener>> hashMap, int id, ContactData c) {
		ArrayList<ContactEventListener> list = hashMap.get(id);
		if (list == null) return;
		//System.out.println(list.size());
		for (int i=0; i<list.size(); ++i) {
			list.get(i).trigger(c);
		}
	}
	
	private void registerListener(HashMap<Integer,ArrayList<ContactEventListener>> hashMap, int id1, int id2, ContactEventListener listener) {
		int hashed = hash(id1,id2);
		if (hashMap.containsKey(hashed)) {
			hashMap.get(hashed).add(listener);
		} else {
			ArrayList<ContactEventListener> list = new ArrayList<ContactEventListener>();
			list.add(listener);
			hashMap.put(hashed, list);
		}
	}
	
	private void unregisterListener(HashMap<Integer,ArrayList<ContactEventListener>> hashMap, int id1, int id2, ContactEventListener listener) {
		int hashed = hash(id1,id2);
		if (hashMap.containsKey(hashed)) {
			ArrayList<ContactEventListener> list = hashMap.get(hashed);
			if (list.remove(listener)) {
				// Success - check if the arraylist is now empty, in which case
				// we can remove the arraylist from the map.
				if (list.size() == 0) hashMap.remove(hashed);
			} else {
				throw new RuntimeException("Attempted to remove listener which was not found in array list.");
			}
		} else {
			throw new RuntimeException("Key was not found in hash map.  " +
					"This probably means no listeners are registered for the body pair.");
		}
	}
	
	/**
	 * Register a listener to respond to all events involving an Actor.
	 * Use Actor::getID() to get id numbers for Actors. 
	 * @param id
	 * @param listener
	 */
	public void registerActorListener(int id, ContactEventListener listener) {
		m_dispatchEvents = true;
		if (actorListeners.containsKey(id)) {
			actorListeners.get(id).add(listener);
		} else {
			ArrayList<ContactEventListener> list = new ArrayList<ContactEventListener>();
			list.add(listener);
			actorListeners.put(id, list);
		}
	}
	
	/**
	 * Unregister a previously registered listener.  Throws
	 * a runtime exception if no matching listener has been
	 * registered.  
	 * @param id
	 * @param listener
	 */
	public void unregisterActorListener(int id, ContactEventListener listener) {
		m_dispatchEvents = true;
		if (actorListeners.containsKey(id)) {
			ArrayList<ContactEventListener> list = actorListeners.get(id);
			if (list.remove(listener)) {
				if (list.size() == 0) actorListeners.remove(id);
			} else {
				throw new RuntimeException("Attempted to remove listener which was not found in array list.");
			}
		} else {
			throw new RuntimeException("Key was not found in hash map.  " +
			"This probably means no listeners are registered for the body.");
		}
	}
	
	/**
	 * Register a listener that responds to all events between a given
	 * actor and group.
	 * @param actorID
	 * @param groupNumber
	 * @param listener
	 */
	public void registerActorGroupListener(int actorID, int groupNumber, ContactEventListener listener) {
		m_dispatchEvents = true;
		registerListener(actorGroupListeners,actorID,groupNumber,listener);
	}
	
	/**
	 * Unregister a previously registered listener.  Throws
	 * a runtime exception if no matching listener has been
	 * registered.  
	 * @param actorID
	 * @param groupNumber
	 * @param listener
	 */
	public void unregisterActorGroupListener(int actorID, int groupNumber, ContactEventListener listener) {
		m_dispatchEvents = true;
		unregisterListener(actorGroupListeners,actorID,groupNumber,listener);
	}
	
	/**
	 * Registers a contact event listener to respond to events between
	 * actors with specified id's (use Actor::getID() to find these).
	 * Do not register the same listener more than once for the same pair!
	 * Order does not matter - the listener is registered for both orderings.
	 * @param id1
	 * @param id2
	 * @param listener
	 */
	public void registerActorActorListener(int id1, int id2, ContactEventListener listener) {
		m_dispatchEvents = true;
		registerListener(actorActorListeners,id1,id2,listener);
		if (id2 != id1) registerListener(actorActorListeners,id2,id1,listener);
	}
	
	
	/**
	 * Unregister a specific actor-actor pair listener.
	 * @param id1 First id in pair
	 * @param id2 Second id in pair
	 * @param listener Listener to remove
	 */
	public void unregisterActorActorListener(int id1, int id2, ContactEventListener listener) {
		m_dispatchEvents = true;
		unregisterListener(actorActorListeners,id1,id2,listener);
		if (id1 != id2) unregisterListener(actorActorListeners,id2,id1,listener);
	}
	
	/**
	 * Registers a contact event listener to respond to events between
	 * actors registered in these groups.
	 * Do not register the same listener more than once for the same pair!
	 * Order does not matter - the listener is registered for both orderings.
	 * @param group1
	 * @param group2
	 * @param listener
	 */
	public void registerGroupGroupListener(int group1, int group2, ContactEventListener listener) {
		m_dispatchEvents = true;
		registerListener(groupGroupListeners,group1,group2,listener);
		if (group2 != group1) registerListener(groupGroupListeners,group2,group1,listener);
	}
	
	/**
	 * Unregister a group-group pair listener.
	 * @param group1 First group in pair
	 * @param group2 Second group in pair
	 * @param listener Listener to remove
	 */
	public void unregisterGroupGroupListener(int group1, int group2, ContactEventListener listener) {
		m_dispatchEvents = true;
		unregisterListener(groupGroupListeners,group1,group2,listener);
		if (group1 != group2) unregisterListener(groupGroupListeners,group2,group1,listener);
	}

	
	/**
	 * Two element (int,int)->int hash.
	 * 
	 * @param id1 First value in pair to hash
	 * @param id2 Second value in pair to hash
	 * @return Hash value
	 */
	static public final int hash(int id1, int id2) {
        int key = (id2 << 16) | id1;
        key = ~key + (key << 15);
        key = key ^ (key >>> 12);
        key = key + (key << 2);
        key = key ^ (key >>> 4);
        key = key * 2057;
        key = key ^ (key >>> 16);
        return key;
    }

	/**
	 * Dispatch events (if enabled).  Usually the user
	 * should not need to call this function manually.
	 */
	public void step(float dt, int iterations) {
		if (m_dispatchEvents) emitEvents();
	}
	
	/**
	 * Emit all events to all registered listeners.
	 * Assumes all bodies and shapes have valid
	 * userdata of type Actor and ShapeData, resp.
	 * <p>
	 * After event emission, the contact store is cleared
	 * to make room for more events if clearContactStore
	 * is true.
	 * </p>
	 * @param clearContactStore Should the contact store be cleared after emission?
	 */
	public void emitEvents(boolean clearContactStore) {
		int pointsToSend = contactStore.getContactPointCount();
		ContactData[] points = contactStore.getContactPoints();
		for (int i=0; i<pointsToSend; ++i) {
			dispatchEvent(points[i]);
		}
		contactStore.clearContactPoints();
	}
	
	/**
	 * Emit all events to all registered listeners.
	 * Assumes all bodies and shapes have valid
	 * userdata of type Actor and ShapeData, resp.
	 * <p>
	 * After event emission, the contact store is cleared
	 * to make room for more events.
	 * </p>
	 */
	public void emitEvents() {
		emitEvents(true);
	}
}
